package doc.ic.walkmore.services;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map.Entry;

import weka.classifiers.Classifier;
import weka.core.Instances;
import weka.classifiers.lazy.IB1;

import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.util.Log;

import doc.ic.walkmore.DAO.StepsDataSource;
import doc.ic.walkmore.DAO.StepsDataSourceWrittable;
import doc.ic.walkmore.toolbox.ToolBox;
import doc.ic.walkmore.weka.ClassifierBuilder;
import doc.ic.walkmore.weka.DataPoint;
import doc.ic.walkmore.weka.DataWindow;
import doc.ic.walkmore.weka.FileLoader;


public class ActivitySensorService extends Service implements SensorEventListener {

	private static final String LOGTAG = "ActivitySensorService";
	protected static final long SCREEN_OFF_RECEIVER_DELAY = 500;//Milliseconds to wait after screen off
	
	private long mLastEventTimestamp = 0;
	
	// The classifier used by this service
	private ClassifierBuilder classifierb;
	
	// current data window to process
	private DataWindow mDataWindow;
	
	// Our data extract
	private int mSteps = 0;
	private String mActivity = "";
	private long mTimeElapsed = 0l; // in NANOSECONDS
	
	private boolean isSensing = false;
	
	// Count number of datawindow empty to stop the service in case of long inactivity
	private int nStill;
	
	// Sensor manager service
	private SensorManager mSensorManager;
    
	// PowerManager and WaveLock
	private PowerManager pm;
	private PowerManager.WakeLock wl;
	
	// DB
	private StepsDataSourceWrittable datasource;

    // Binder given to clients
    private final IBinder mBinder = new LocalBinder();
    
    /**
     * Class used for the client Binder.  Because we know this service always
     * runs in the same process as its clients, we don't need to deal with IPC.
     */
    public class LocalBinder extends Binder {
    	public ActivitySensorService getService() {
            // Return this instance of ActivitySensorService so clients can call public methods
            return ActivitySensorService.this;
        }
    }
    
    // My receiver, after 500ms of SCREEN OFF, try to unregister then re-register the sensor
    public BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            Log.i(LOGTAG, "onReceive("+intent+")");

            if (!intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
                return;
            }
             
            Runnable runnable = new Runnable() {
                public void run() {
                    Log.i(LOGTAG, "Runnable executing.");
                    unregisterListener();
                    registerListener();
                }
            };

            if (isSensing)
            	new Handler().postDelayed(runnable, SCREEN_OFF_RECEIVER_DELAY);
        }
    };

    
	// On creation of this service	
    @Override
    public void onCreate() {        
	   	// Get powerManager
	   	pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
	   	wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "sensing");
	   	
        // Get sensor manager
		mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
		
		// Create classifier
		try {
		InputStream training = getAssets().open("trainingSetNew.arff");
		Classifier algo = new IB1(); // IB1 performs better than NaiveBayes
		Instances trainingSet = FileLoader.getDataSetFromFile(training);
		classifierb = (new ClassifierBuilder(algo)).build(trainingSet);
		training.close();
		} catch(IOException e1) {
			// File can't be read/close
			e1.printStackTrace();
		} catch(Exception e2) {
			// Classifier not built
			e2.printStackTrace();
		}
		
		// Initialize first DW
		mDataWindow = new DataWindow(classifierb);
    }
    
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i(LOGTAG, "Received start id " + startId + ": " + intent);
        // We want this service to continue running until it is explicitly
        // stopped, so return sticky.
    	return START_STICKY;
    }
    
    public void startSensing() {
    	if (!isSensing) {
    		mTimeElapsed = 0;
    		mLastEventTimestamp = 0;
    		
	        // Register a receiver on SCREEN OFF event
	        registerReceiver(mReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
	        
	    	registerListener();
	        // Aquire wave lock
	    	wl.acquire();
	    	
	    	Log.i(LOGTAG, "Service started sensing");
	    	
	    	isSensing = true;
	    	datasource.warnStartSensing(); // So we can prepare anything we want
    	}
    }
    
    private void registerListener() {
		// Register the sensor listener
	    mSensorManager.registerListener(this,
	        mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
	        SensorManager.SENSOR_DELAY_GAME);    	
    }
    
    private void unregisterListener() {
		// Unregister the listener
		mSensorManager.unregisterListener(this);
    }
	
	@Override
	public void onAccuracyChanged(Sensor arg0, int arg1) {
	}

	@Override
	public void onSensorChanged(SensorEvent event) {
        synchronized (this) {
            if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
            	// /!\ event.timestamp is in NANOseconds!
            	long timeSinceLastEvent = event.timestamp - mLastEventTimestamp;
            	if (timeSinceLastEvent > ToolBox.SAMPLING_PERIOD_MS*1000000) {
            		// If it's NOT the first event
            		if (mLastEventTimestamp != 0)
            			mTimeElapsed += timeSinceLastEvent;
	        		// Save data in data structure
			        DataPoint point = new DataPoint();
	        		point.setTimestamp(event.timestamp/1000000);
	        		point.setX(event.values[0]);
	        		point.setY(event.values[1]);
	        		point.setZ(event.values[2]);
	        		if (mDataWindow.hasSpace()) 
	        			mDataWindow.addDataPoint(point);
	        		else {
	        			// Filled a Datawindow, let's process,
	        			// Results stored in mActivity and mSteps
	        			process(mDataWindow);
	        			// update DB
        	    		addStepsInDb(mActivity, mSteps, mTimeElapsed/1000000);
	        	    		
	        			Log.i(LOGTAG, "Still sensing! nStill = "+nStill+", mSteps = " +mSteps+", mTimeElapsed = "+mTimeElapsed);
	        			mTimeElapsed = 0;
	        		}

	        		// Stop sensing in case of inactivity
	    			if (nStill > 2) {
	    				stopSensing();
	    			}
	        		
	             	mLastEventTimestamp = event.timestamp;	
            	}
            }
        }
	}

	/**
	 * Adds the processed Datawindow in the DataBase
	 * @param activity
	 * @param number of steps
	 * @param duration in milliseconds
	 */
	private void addStepsInDb(String activity, long number, long time) {
		if (number>0) {
			SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
			String now = sdf.format(new Date());
			// ====== /!\ ============
			// Step length hard coded
			int stepLength = 60; //cm
			// ====== /!\ ============
			long distance = number*stepLength;
			
			datasource.createSteps(number, time, activity, distance, now);
		}
	}
	
	// Might be too heavy to be processed in the sensorChanged event?
	/**
	 * Processes the datawindow to classify the activity performed
	 * And returns the nb of steps done
	 * @precondition datawindow needs to be full
	 */
	synchronized private int process(DataWindow dw) {
		int steps = 0;
		String activity = "No significant movement detected";
		if (dw.isMouvement()) {
			nStill = 0;
			// Classify activity
			double [] likelihood = null;
			double prob_classe = 0;
			try {
				likelihood = dw.likelihood();
				Entry<Integer,Double> max = ToolBox.max(likelihood);
				prob_classe = max.getValue();
				activity = ToolBox.classes[max.getKey()];
			} catch (Exception e) {
				// Unable to classify, what shall we do?
				e.printStackTrace();
			}
			
			// Count steps only if the activity detected has a high probability classification
//			if (prob_classe>0.99) {
				// number of steps counted
				steps = dw.countSteps();
//				Log.i(TAG, "Steps counted :"+steps);
//			}
			// ===========================
			
		} else {
			// No movement
			// What shall we do?
			// We should warn the DetectMovementService and stop sensing with this one
			// Let's count the number of empty datawindow
			// After 14sec, consider the device still (so approx 2 datawindows of 7sec)
			nStill++;
			if (nStill>1) stopSensing();
		}
		// Once processed empty dw
		mDataWindow = new DataWindow(classifierb);
		
		// Update fields
		mSteps = steps;
		mActivity = activity;

		return steps;
	}
	
	
	
	@Override
	public IBinder onBind(Intent intent) {
		 return mBinder;
	}
	
    
    // We have some datapoints from the detectMvt service
    public void fillDataWindow(String filename) {
        // Read the file
        try {
                FileInputStream fis = openFileInput(filename);
                // if file the available for reading
                if (fis != null) {
                  // prepare the file for reading
                  InputStreamReader inputreader = new InputStreamReader(fis);
                  BufferedReader buffreader = new BufferedReader(inputreader);

                  String line;
                  
                  //skip first line
                  buffreader.readLine();
                  while(buffreader.ready()) {
                          line = buffreader.readLine();
                          String[] parser = line.split(",");
                          // Process one line and save in a DataPoint
                          try {
                                  DataPoint point = new DataPoint();
                                  point.setTimestamp(Long.parseLong(parser[0]));
                                  point.setX(Float.parseFloat(parser[1]));
                                  point.setY(Float.parseFloat(parser[2]));
                                  point.setZ(Float.parseFloat(parser[3]));
                                  
                                  if (mDataWindow.hasSpace())
                                	  mDataWindow.addDataPoint(point); //Add to datawindow
                          } catch (NumberFormatException e) {
                                  e.printStackTrace();
                          }
                          
                  }
                  
                  fis.close();
                }
        } catch (IOException e) {
                e.printStackTrace();                    
        }
        deleteFile(filename);
    }

	
	public void stopSensing() {
		if (isSensing) {
			unregisterReceiver(mReceiver);
			unregisterListener();
	        // Release wave lock
	    	wl.release();
	    	
	    	Log.i(LOGTAG, "Service stopped sensing");
	    	
	    	isSensing = false;
	    	
			// count the last steps in the current datawindow
			mDataWindow.complete();
			 
			process(mDataWindow); // Updates mSteps & mActivity field
			// Update DB
    		addStepsInDb(mActivity, mSteps, mTimeElapsed/1000000);	
    		datasource.warnStopSensing(); // And warn it's the end for the moment
		}
	}
	    
    public void reset() {
    	mSteps = 0;
    	mDataWindow = new DataWindow(classifierb);
    	nStill = 0;
    }
	
	@Override
	public void onDestroy() {
		if (isSensing) {
			stopSensing();
		}
        
        Log.i(LOGTAG, "service terminated");
        
        super.onDestroy();
	}
	
	public int getmSteps() {
		return mSteps;
	}

	public String getmActivity() {
		return mActivity;
	}
	

	public boolean isSensing() {
		return isSensing;
	}

	public void setDataSource(StepsDataSourceWrittable datasrc) {
		datasource = datasrc;
	}
}

